package com.jihong.plus;

import com.jihong.plus.util.EncryptHandler;
import com.jihong.plus.util.MyConfig;
import com.jihong.plus.util.SqlParse;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;

import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 思考：
 * 拦截 Executor.update or  Executor.query 问题，这时候boundSql还未创建，那为啥不直接修改sql，因为sqlSource中sql可能是动态标签
 * 拦截 ParameterHandler 问题，这时候boundSql已经给 connection 赋值了sql语句，这个时候修改boundSql也无济于事
 */
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,Integer.class}),
})
public class ParamInterceptor implements Interceptor {
    /**
     * 1、新增一个新字段兼容存量数据
     * 2、拦截器增量数据双写，旧字段写明文，新字段写密文
     * 3、开启一个定时任务，批量更新存量数据密文
     * 4、拦截器读请求支持开关，等存量数据全部更新完，才读密文，否则全部流量还是读明文。
     * 5、待批量更新存量数据的明文字段更新完毕
     * 6、打开拦截器读请求开关，后续全部的流量都走密文，明文不走了。
     * 7、开启定时任务，清除明文字段的数据
     *
     *
     * 拦截器实现思路：
     * 1-1、如果是双写，查询操作跟还是查询旧字段，那么需要在 insert 和 update 都需要在之前的基础上，还有需要加上额外的加密字段，比如【update table set phone = 123 where phone = 456】 ,那么需要替换为【update table set phone = 123,phone_en = 123***^^ where phone = 456】
     * 1-2、如果不是双写，查询、删除、新增和修改，都需要的把原来的字段替换为加密字段
     * 2、Mybatis一个占位符[?]对应 boundSql中 ParameterMappings，每次遍历获取占位符[?]拿到当前Mysql字段，如果需要加密
     *       2-1、那么判断是否是双写，那么对字段进行拼接，参数进行加密操作，然后对sql进行拼接
     *       2-2、如果不是双写，那么直接替换需要加密的字段，然后把需要加密的 ParameterMapping 存起来
     * 3、把需要加密的 ParameterMapping 进行遍历，ParameterMapping 的 property 字段可以通过BoundSql.additionalParameters(mybatis处理动态标签的附加参数)和ParameterHandler.parameterObject（@Param方法上传入的参数）获取到实际的值
     * 4、处理sql语句和参数加密到这就结束，至此处理参数拦截器就结束了
     * 5、接下来要针对查询语句的返回结构做处理，比如select phone_en from table，phone_en由于历史原因，旧实体是没有加密字段的，需要把旧字段的数据进行解密操作
     * 6、拦截 ResultSetHandler.handleResultSets,这个阶段jdbc已经查询sql获取到数据，只是数据还没解析，而这个方法的参数是Statement
     * 7、获取到 Statement 获取到 ColumnDefinition的字段信息，这个字段就是mysql查出来就是你select 的字段，如果是的*号就是你表中全部的字段
     * 8、遍历全部select的字段，如果是已经加密的字段，那么把这个加密的字段替换为之前字段，比如phone_en替换为phone
     * 9、替换完字段，让mybatis去执行，拦截器放行获取到数据，这时候返回一个List<Object>对象
     * 10、遍历这个集合，获取里面的字段，是否是需要加密的，如果是的话进行解密操作
     *
     * 疑问
     * ParameterMapping是什么，他是什么时候生成的？
     * 答：ParameterMapping主要保存#{id}中信息，mybatis解析我们sql语句时，会把我们#{XX}中的信息封装成一个个ParameterMapping，然后替换成?,后续一个ParameterMapping就代表一个？，
     * 如果是动态sql，那么在执行过程中才生成ParameterMapping，如果不是在解析xml就已经生成了
     *    ParameterMapping如何和我们的参数对应上的？
     *     答：由ParameterHandler.parameterObject和BoundSql.additionalParameters，这两个对象就是Map，key对应ParameterMapping.property属性，value就是占位符真实的值。
     *     占位符实际的值就是我们传递的参数，比如我们传的是基本类型，@Param("id"),那么会跟#{id}联系上，如果是实体user,那么就是#{user.id},.parameterObject和additionalParameters
     *     区别就是，parameterObject的key是我们用户传递的参数，而additionalParameters是mybatis动态sql生成的，比如你在xml foreach中item属性，mybatis会生成当前item唯一的字符串，
     *     用来绑定ParameterMapping.property和BoundSql.additionalParameters中的参数
     *    ResultSetHandler如何跟Statement对应上的？
     *    答：Statement查询到的是select的数据库表字段，如果是*的话就是表中全部的字段，mybatis拿到字段，会拿到Configuration的驼峰配置，然后把数据映射到resultType去。

     *

     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MetaObject metaObject = SystemMetaObject.forObject(invocation);
        BoundSql boundSql = (BoundSql) metaObject.getValue("target.delegate.boundSql");
        //获取需要加解密的配置
        List<MyConfig.EncryptData> needEnFieldList = MyConfig.getNeedEnField();
        //获取参数的MetaObject
        MetaObject paramMetaObject = getParamMetaObject(boundSql,metaObject);
        //解析sql和替换sql
        SqlParse sqlParse = new SqlParse(boundSql.getSql(), boundSql.getParameterMappings(),needEnFieldList,paramMetaObject);
        //获取需要加密的数据进行加密
        List<ParameterMapping> needEncryptParameterMapping = sqlParse.getNeedEncryptParameterMapping();
        for (ParameterMapping parameterMapping : needEncryptParameterMapping) {
            String property = parameterMapping.getProperty();
            Object value = paramMetaObject.getValue(property);
            paramMetaObject.setValue(property, EncryptHandler.encryptValue(String.valueOf(value)));
        }
        //修改sql
        MetaObject sqlMetaObject = SystemMetaObject.forObject(boundSql);
        sqlMetaObject.setValue("sql",sqlParse.getUpdateSql());
        return invocation.proceed();
    }

    /**
     * 获取参数ParamMetaObject
     * @param boundSql
     * @param metaObject
     * @return
     */
    private MetaObject getParamMetaObject(BoundSql boundSql,MetaObject metaObject) {
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("target.delegate.mappedStatement");
        Object paramObject =  metaObject.getValue("target.delegate.parameterHandler.parameterObject");
        Configuration configuration = mappedStatement.getConfiguration();
        MetaObject paramMetaObject = configuration.newMetaObject(paramObject);
        MetaObject boundSqlMeta = SystemMetaObject.forObject(boundSql);
        Map<String, Object> additionalParameters = (Map<String, Object>) boundSqlMeta.getValue("additionalParameters");
        if (additionalParameters != null && additionalParameters.size() > 0){
            additionalParameters.forEach((k,v) -> paramMetaObject.setValue(k,v));
        }
        return paramMetaObject;
    }
}